package busterminal.entity.service;

import busterminal.exception.BusinessException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;

/**
 * Класс, описывающий время
 */
public class Time {

    /**
     * Часы
     */
    private int hours;
    /**
     * Минуты
     */
    private int minutes;
    /**
     * Регулярное выражение для проверки корректности ввода времени в формате <code>HH:MM</code>
     */
    private static final String CORRECT_TIME_FORMAT = "^(([0-1][0-9])|([2][0-3])):([0-5][0-9])$";

    /**
     * Конструктор по умолчанию
     */
    public Time() {
        hours = 0;
        minutes = 0;
    }

    /**
     * Параметризованный конструктор
     * @param hours часы
     * @param minutes минуты
     * @throws BusinessException неверно заданы часы и/или минуты
     */
    public Time(int hours, int minutes) throws BusinessException {
        if (hours >= 0 && hours <= 23) {
            this.hours = hours;
        } else {
            throw new BusinessException("Incorrect hours '" + hours + "'");
        }
        if (minutes >= 0 && minutes <= 59) {
            this.minutes = minutes;
        } else {
            throw new BusinessException("Incorrect minutes '" + minutes + "'");
        }
    }

    /**
     * Преобразовать строку в формате <code>HH:MM</code> в объект типа {@link Time}
     * @param s строка в формате <code>HH:MM</code>
     * @throws BusinessException некорректная входная строка
     */
    public static Time parseTime(String s) throws BusinessException {
        if (!s.trim().matches(CORRECT_TIME_FORMAT)) {
            throw new BusinessException("Incorrect time '" + s + "'");
        }
        StringTokenizer st = new StringTokenizer(s.trim(), ":");
        int h = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());
        return new Time(h, m);
    }

    /**
     * Вычисление разности между текущим и указанным объектом {@link Time}
     * @param anotherTime объект класса {@link Time}, значение которого будет вычтено из значения текущего объекта
     * @throws BusinessException время текущего объекта больше времени указанного
     */
    public Time getDifference(Time anotherTime) throws BusinessException {
        int fullMinutes1 = this.hours * 60 + this.minutes;
        int fullMinutes2 = anotherTime.hours * 60 + anotherTime.minutes;
        int difference = fullMinutes1 - fullMinutes2;

        if (difference < 0) {
            throw new BusinessException("Time difference cannot be less than zero");
        }

        if (difference < 60) {
            return new Time(0, difference);
        } else {
            int h = difference / 60 - (difference / 60) % 1;
            int m = difference - 60 * h;
            return new Time(h, m);
        }
    }

    /**
     * Проверить, находится ли текущий объект в указанном диапазоне до другого объекта
     * @param anotherTime другой объект {@link Time}
     * @param amount диапазон в виде {@link Time}
     * @return true - текущий объект попадает в интервал, false - не попадает
     */
    public boolean isBefore(Time anotherTime, Time amount) {
        Calendar anotherCalendar = toCalendar(anotherTime);
        Calendar thisCalendar = toCalendar(this);

        anotherCalendar.add(Calendar.HOUR_OF_DAY, -amount.hours);
        anotherCalendar.add(Calendar.MINUTE, -amount.minutes);

        return thisCalendar.compareTo(anotherCalendar) > 0;
    }

    /**
     * Проверить, находится ли текущий объект в указанном диапазоне после другого объекта
     * @param anotherTime другой объект {@link Time}
     * @param amount диапазон в виде {@link Time}
     * @return true - текущий объект попадает в интервал, false - не попадает
     */
    public boolean isAfter(Time anotherTime, Time amount) {
        Calendar anotherCalendar = toCalendar(anotherTime);
        Calendar thisCalendar = toCalendar(this);

        anotherCalendar.add(Calendar.HOUR_OF_DAY, amount.hours);
        anotherCalendar.add(Calendar.MINUTE, amount.minutes);

        return thisCalendar.compareTo(anotherCalendar) < 0;
    }

    /**
     * Проверить, находится ли текущий объект в указанных диапазонах до и после другого объекта
     * @param anotherTime другой объект {@link Time}
     * @param amountBefore диапазон "до" в виде {@link Time}
     * @param amountAfter диапазон "после" в виде {@link Time}
     * @return true - текущий объект попадает в интервал, false - не попадает
     */
    public boolean isWithinRange(Time anotherTime, Time amountBefore, Time amountAfter) {
        return isBefore(anotherTime, amountBefore) && isAfter(anotherTime, amountAfter);
    }

    /**
     * Получить объект {@link Time}, содержащий текущее время
     * @return объект {@link Time}, содержащий текущее время
     */
    public static Time now() {
        try {
            return Time.parseTime(new SimpleDateFormat("HH:mm").format(new Date()));
        } catch (BusinessException e) {
            return new Time(0, 0);
        }
    }

    /**
     * Получить количество часов
     * @return количество часов
     */
    public int getHours() {
        return hours;
    }

    /**
     * Установить значение "часы"
     * @param hours новое значение
     * @throws BusinessException неверно заданы часы
     */
    public void setHours(int hours) throws BusinessException {
        if (hours >= 0 && hours <= 23) {
            this.hours = hours;
        } else {
            throw new BusinessException("Incorrect hours '" + hours + "'");
        }
    }

    /**
     * Получить количество минут
     * @return количество минут
     */
    public int getMinutes() {
        return minutes;
    }

    /**
     * Установить значение "минуты"
     * @param hours новое значение
     * @throws BusinessException неверно заданы минуты
     */
    public void setMinutes(int minutes) throws BusinessException {
        if (minutes >= 0 && minutes <= 59) {
            this.minutes = minutes;
        } else {
            throw new BusinessException("Incorrect minutes '" + minutes + "'");
        }
    }

    /**
     * Преобразовать текущий объект {@link Time} к объекту {@link java.util.Calendar}
     * @param time объект {@link Time}
     * @return объект {@link java.util.Calendar}
     */
    public static Calendar toCalendar(Time time) {
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(2000, 0, 1, time.hours, time.minutes);
        return calendar;
    }

    /**
     * Преобразовать объект {@link java.util.Calendar} к объекту {@link Time}
     * @param calendar объект {@link java.util.Calendar}
     * @return объект {@link Time}
     */
    public static Time toTime(Calendar calendar) {
        return Time.parseTime(new SimpleDateFormat("HH:mm").format(calendar));
    }

    /**
     * Метод, возвращающий текстовое представление объекта {@link Time} в формате <code>HH:MM</code>
     * @return текстовое представление объекта
     */
    @Override
    public String toString() {
        return String.format("%02d:%02d", hours, minutes);
    }
}
